package com.ogawa.a7517.widget;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Typeface;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;

import androidx.core.widget.ScrollerCompat;

import com.ogawa.a7517.R;

/**
 * Created by Carbs.Wang.
 * email : yeah0126@yeah.net
 * github : https://github.com/Carbs0126/NumberPickerView
 * Created by HourGlassRemember on 2018/10/25.
 */
public class NumberPickerView extends View {

    private static final int DEFAULT_TEXT_COLOR_NORMAL = 0XFF333333;// default text color of not selected item
    private static final int DEFAULT_TEXT_COLOR_SELECTED = 0XFFF56313;// default text color of selected item
    private static final int DEFAULT_TEXT_SIZE_NORMAL_SP = 14;// default text size of normal item
    private static final int DEFAULT_TEXT_SIZE_SELECTED_SP = 16;// default text size of selected item
    private static final int DEFAULT_TEXT_SIZE_HINT_SP = 14;// default text size of hint text, the middle item's right text
    private static final int DEFAULT_MARGIN_START_OF_HINT_DP = 8;// distance between selected text and hint text
    private static final int DEFAULT_MARGIN_END_OF_HINT_DP = 8;// distance between hint text and right of this view, used in wrap_content mode
    private static final int DEFAULT_DIVIDER_COLOR = 0XFFF56313;// default divider's color
    private static final int DEFAULT_DIVIDER_HEIGHT = 2;// default divider's height
    private static final int DEFAULT_DIVIDER_MARGIN_HORIZONTAL = 0;// default divider's margin to the left & right of this view
    private static final int DEFAULT_SHOW_COUNT = 3;// default shown items' count, now we display 3 items, the 2nd one is selected
    private static final int DEFAULT_ITEM_PADDING_DP_H = 5;// default items' horizontal padding, left padding and right padding are both 5dp, only used in wrap_content mode
    private static final int DEFAULT_ITEM_PADDING_DP_V = 2;// default items' vertical padding, top padding and bottom padding are both 2dp, only used in wrap_content mode
    private static final int HANDLER_WHAT_REFRESH = 1;// message's what argument to refresh current state, used by mHandler
    private static final int HANDLER_WHAT_LISTENER_VALUE_CHANGED = 2;// message's what argument to respond value changed event, used by mHandler
    private static final int HANDLER_WHAT_REQUEST_LAYOUT = 3;// message's what argument to request layout, used by mHandlerInMainThread
    // interval time to scroll the distance of one item's height
    private static final int HANDLER_INTERVAL_REFRESH = 32;//millisecond
    private static final int DEFAULT_INTERVAL_REVISE_DURATION = 300;// in millisecond unit, default duration of scrolling an item' distance
    private static final int DEFAULT_MIN_SCROLL_BY_INDEX_DURATION = DEFAULT_INTERVAL_REVISE_DURATION;// max and min durations when scrolling from one value to another
    private static final int DEFAULT_MAX_SCROLL_BY_INDEX_DURATION = DEFAULT_INTERVAL_REVISE_DURATION * 2;
    private static final String TEXT_ELLIPSIZE_START = "start";
    private static final String TEXT_ELLIPSIZE_MIDDLE = "middle";
    private static final String TEXT_ELLIPSIZE_END = "end";
    private static final boolean DEFAULT_SHOW_DIVIDER = true;
    private static final boolean DEFAULT_WRAP_SELECTOR_WHEEL = true;
    private static final boolean DEFAULT_CURRENT_ITEM_INDEX_EFFECT = false;
    private static final boolean DEFAULT_RESPOND_CHANGE_ON_DETACH = false;
    private static final boolean DEFAULT_RESPOND_CHANGE_IN_MAIN_THREAD = true;

    private int mTextColorNormal = DEFAULT_TEXT_COLOR_NORMAL;
    private int mTextColorSelected = DEFAULT_TEXT_COLOR_SELECTED;
    private int mTextColorHint = DEFAULT_TEXT_COLOR_SELECTED;
    private int mTextSizeNormal = 0;
    private int mTextSizeSelected = 0;
    private int mTextSizeHint = 0;
    private int mWidthOfHintText = 0;
    private int mWidthOfAlterHint = 0;
    private int mMarginStartOfHint = 0;
    private int mMarginEndOfHint = 0;
    private int mItemPaddingVertical = 0;
    private int mItemPaddingHorizontal = 0;
    private int mDividerColor = DEFAULT_DIVIDER_COLOR;
    private int mDividerHeight = DEFAULT_DIVIDER_HEIGHT;
    private int mDividerMarginL = DEFAULT_DIVIDER_MARGIN_HORIZONTAL;
    private int mDividerMarginR = DEFAULT_DIVIDER_MARGIN_HORIZONTAL;
    private int mShowCount = DEFAULT_SHOW_COUNT;
    private int mMinShowIndex = -1;
    private int mMaxShowIndex = -1;
    //compat for android.widget.NumberPicker
    private int mMinValue = 0;
    //compat for android.widget.NumberPicker
    private int mMaxValue = 0;
    private int mMaxWidthOfDisplayedValues = 0;
    private int mMaxHeightOfDisplayedValues = 0;
    private int mMaxWidthOfAlterArrayWithMeasureHint = 0;
    private int mMaxWidthOfAlterArrayWithoutMeasureHint = 0;
    private int mPrevPickedIndex = 0;
    private int mMiniVelocityFling = 150;
    private int mScaledTouchSlop = 8;
    private String mHintText;
    private String mTextEllipsize;
    private String mEmptyItemHint;
    private String mAlterHint;
    private float mFriction = 1f;//friction used by scroller when fling
    private float mTextSizeNormalCenterYOffset = 0f;
    private float mTextSizeSelectedCenterYOffset = 0f;
    private float mTextSizeHintCenterYOffset = 0f;
    private boolean mShowDivider = DEFAULT_SHOW_DIVIDER;//true to show the two dividers
    private boolean mWrapSelectorWheel = DEFAULT_WRAP_SELECTOR_WHEEL;//true to wrap the displayed values
    private boolean mCurrentItemIndexEffect = DEFAULT_CURRENT_ITEM_INDEX_EFFECT;//true to set to the current position, false set position to 0
    private boolean mHasInit = false;//true if NumberPickerView has initialized
    private boolean mWrapSelectorWheelCheck = true;// if displayed values' number is less than show count, then this value will be false.
    private boolean mPendingWrapToLinear = false;// if you want you set to linear mode from wrap mode when scrolling, then this value will be true.
    // if this view is used in same dialog or PopupWindow more than once, and there are several NumberPickerViews linked, such as Gregorian Calendar with MonthPicker and DayPicker linked, set mRespondChangeWhenDetach
    // true to respond onValueChanged callbacks if this view is scrolling when detach from window, but this solution is unlovely and may cause NullPointerException (even i haven't found this NullPointerException),
    // so I highly recommend that every time setting up a reusable dialog with a NumberPickerView in it, please initialize NumberPickerView's data, and in this way, you can set mRespondChangeWhenDetach false.
    private boolean mRespondChangeOnDetach = DEFAULT_RESPOND_CHANGE_ON_DETACH;
    // this is to set which thread to respond onChange... listeners including OnValueChangeListener, OnValueChangeListenerRelativeToRaw and OnScrollListener when view is scrolling or starts to scroll or stops scrolling.
    private boolean mRespondChangeInMainThread = DEFAULT_RESPOND_CHANGE_IN_MAIN_THREAD;
    private ScrollerCompat mScroller;
    private VelocityTracker mVelocityTracker;
    private Paint mPaintDivider = new Paint();
    private Paint mPaintHint = new Paint();
    private TextPaint mPaintText = new TextPaint();
    private String[] mDisplayedValues;
    private CharSequence[] mAlterTextArrayWithMeasureHint;
    private CharSequence[] mAlterTextArrayWithoutMeasureHint;
    private HandlerThread mHandlerThread;
    private Handler mHandlerInNewThread;
    private Handler mHandlerInMainThread;
    private OnValueChangeListenerRelativeToRaw mOnValueChangeListenerRaw;
    private OnValueChangeListener mOnValueChangeListener; //compatible for NumberPicker
    private OnScrollListener mOnScrollListener;//compatible for NumberPicker
    private OnValueChangeListenerInScrolling mOnValueChangeListenerInScrolling;//response onValueChanged in scrolling
    private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE;// The current scroll state of the NumberPickerView.
    private int mInScrollingPickedOldValue;
    private int mInScrollingPickedNewValue;
    private int mNotWrapLimitYTop;
    private int mNotWrapLimitYBottom;
    private float downYGlobal = 0;
    private float downY = 0;
    private float currY = 0;
    private boolean mFlagMayPress = false;
    private int mViewWidth;
    private int mViewHeight;
    private int mItemHeight;
    private float dividerY0;
    private float dividerY1;
    private float mViewCenterX;
    private int mCurrDrawFirstItemIndex = 0;//first shown item's content index, corresponding to the Index of mDisplayedValued
    private int mCurrDrawFirstItemY = 0;//the first shown item's Y
    private int mCurrDrawGlobalY = 0;//global Y corresponding to scroller
    private int mSpecModeW = MeasureSpec.UNSPECIFIED;
    private int mSpecModeH = MeasureSpec.UNSPECIFIED;

    /**
     * 构造函数——1.View 的初始化
     *
     * @param context 上下文
     */
    public NumberPickerView(Context context) {
        super(context);
        init(context);
    }

    /**
     * 构造函数——1.View 的初始化
     *
     * @param context      上下文
     * @param attributeSet
     */
    public NumberPickerView(Context context, AttributeSet attributeSet) {
        super(context, attributeSet);
        initAttr(context, attributeSet);
        init(context);
    }

    /**
     * 构造函数——1.View 的初始化
     *
     * @param context      上下文
     * @param attributeSet
     * @param defStyleAttr
     */
    public NumberPickerView(Context context, AttributeSet attributeSet, int defStyleAttr) {
        super(context, attributeSet, defStyleAttr);
        initAttr(context, attributeSet);
        init(context);
    }

    private void initAttr(Context context, AttributeSet attributeSet) {
        if (null == attributeSet) {
            return;
        }
        TypedArray typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.NumberPickerView);
        for (int i = 0; i < typedArray.getIndexCount(); i++) {
            int attr = typedArray.getIndex(i);
            if (attr == R.styleable.NumberPickerView_npv_ShowCount) {
                mShowCount = typedArray.getInt(attr, DEFAULT_SHOW_COUNT);
            } else if (attr == R.styleable.NumberPickerView_npv_DividerColor) {
                mDividerColor = typedArray.getColor(attr, DEFAULT_DIVIDER_COLOR);
            } else if (attr == R.styleable.NumberPickerView_npv_DividerHeight) {
                mDividerHeight = typedArray.getDimensionPixelSize(attr, DEFAULT_DIVIDER_HEIGHT);
            } else if (attr == R.styleable.NumberPickerView_npv_DividerMarginLeft) {
                mDividerMarginL = typedArray.getDimensionPixelSize(attr, DEFAULT_DIVIDER_MARGIN_HORIZONTAL);
            } else if (attr == R.styleable.NumberPickerView_npv_DividerMarginRight) {
                mDividerMarginR = typedArray.getDimensionPixelSize(attr, DEFAULT_DIVIDER_MARGIN_HORIZONTAL);
            } else if (attr == R.styleable.NumberPickerView_npv_TextArray) {
                mDisplayedValues = convertCharSequenceArrayToStringArray(typedArray.getTextArray(attr));
            } else if (attr == R.styleable.NumberPickerView_npv_TextColorNormal) {
                mTextColorNormal = typedArray.getColor(attr, DEFAULT_TEXT_COLOR_NORMAL);
            } else if (attr == R.styleable.NumberPickerView_npv_TextColorSelected) {
                mTextColorSelected = typedArray.getColor(attr, DEFAULT_TEXT_COLOR_SELECTED);
            } else if (attr == R.styleable.NumberPickerView_npv_TextColorHint) {
                mTextColorHint = typedArray.getColor(attr, DEFAULT_TEXT_COLOR_SELECTED);
            } else if (attr == R.styleable.NumberPickerView_npv_TextSizeNormal) {
                mTextSizeNormal = typedArray.getDimensionPixelSize(attr, sp2px(context, DEFAULT_TEXT_SIZE_NORMAL_SP));
            } else if (attr == R.styleable.NumberPickerView_npv_TextSizeSelected) {
                mTextSizeSelected = typedArray.getDimensionPixelSize(attr, sp2px(context, DEFAULT_TEXT_SIZE_SELECTED_SP));
            } else if (attr == R.styleable.NumberPickerView_npv_TextSizeHint) {
                mTextSizeHint = typedArray.getDimensionPixelSize(attr, sp2px(context, DEFAULT_TEXT_SIZE_HINT_SP));
            } else if (attr == R.styleable.NumberPickerView_npv_MinValue) {
                mMinShowIndex = typedArray.getInteger(attr, 0);
            } else if (attr == R.styleable.NumberPickerView_npv_MaxValue) {
                mMaxShowIndex = typedArray.getInteger(attr, 0);
            } else if (attr == R.styleable.NumberPickerView_npv_WrapSelectorWheel) {
                mWrapSelectorWheel = typedArray.getBoolean(attr, DEFAULT_WRAP_SELECTOR_WHEEL);
            } else if (attr == R.styleable.NumberPickerView_npv_ShowDivider) {
                mShowDivider = typedArray.getBoolean(attr, DEFAULT_SHOW_DIVIDER);
            } else if (attr == R.styleable.NumberPickerView_npv_HintText) {
                mHintText = typedArray.getString(attr);
            } else if (attr == R.styleable.NumberPickerView_npv_AlternativeHint) {
                mAlterHint = typedArray.getString(attr);
            } else if (attr == R.styleable.NumberPickerView_npv_EmptyItemHint) {
                mEmptyItemHint = typedArray.getString(attr);
            } else if (attr == R.styleable.NumberPickerView_npv_MarginStartOfHint) {
                mMarginStartOfHint = typedArray.getDimensionPixelSize(attr, dp2px(context, DEFAULT_MARGIN_START_OF_HINT_DP));
            } else if (attr == R.styleable.NumberPickerView_npv_MarginEndOfHint) {
                mMarginEndOfHint = typedArray.getDimensionPixelSize(attr, dp2px(context, DEFAULT_MARGIN_END_OF_HINT_DP));
            } else if (attr == R.styleable.NumberPickerView_npv_ItemPaddingVertical) {
                mItemPaddingVertical = typedArray.getDimensionPixelSize(attr, dp2px(context, DEFAULT_ITEM_PADDING_DP_V));
            } else if (attr == R.styleable.NumberPickerView_npv_ItemPaddingHorizontal) {
                mItemPaddingHorizontal = typedArray.getDimensionPixelSize(attr, dp2px(context, DEFAULT_ITEM_PADDING_DP_H));
            } else if (attr == R.styleable.NumberPickerView_npv_AlternativeTextArrayWithMeasureHint) {
                mAlterTextArrayWithMeasureHint = typedArray.getTextArray(attr);
            } else if (attr == R.styleable.NumberPickerView_npv_AlternativeTextArrayWithoutMeasureHint) {
                mAlterTextArrayWithoutMeasureHint = typedArray.getTextArray(attr);
            } else if (attr == R.styleable.NumberPickerView_npv_RespondChangeOnDetached) {
                mRespondChangeOnDetach = typedArray.getBoolean(attr, DEFAULT_RESPOND_CHANGE_ON_DETACH);
            } else if (attr == R.styleable.NumberPickerView_npv_RespondChangeInMainThread) {
                mRespondChangeInMainThread = typedArray.getBoolean(attr, DEFAULT_RESPOND_CHANGE_IN_MAIN_THREAD);
            } else if (attr == R.styleable.NumberPickerView_npv_TextEllipsize) {
                mTextEllipsize = typedArray.getString(attr);
            }
        }
        typedArray.recycle();
    }

    private void init(Context context) {
        mScroller = ScrollerCompat.create(context);
        mMiniVelocityFling = ViewConfiguration.get(getContext()).getScaledMinimumFlingVelocity();
        mScaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
        if (0 == mTextSizeNormal) {
            mTextSizeNormal = sp2px(context, DEFAULT_TEXT_SIZE_NORMAL_SP);
        }
        if (0 == mTextSizeSelected) {
            mTextSizeSelected = sp2px(context, DEFAULT_TEXT_SIZE_SELECTED_SP);
        }
        if (0 == mTextSizeHint) {
            mTextSizeHint = sp2px(context, DEFAULT_TEXT_SIZE_HINT_SP);
        }
        if (0 == mMarginStartOfHint) {
            mMarginStartOfHint = dp2px(context, DEFAULT_MARGIN_START_OF_HINT_DP);
        }
        if (0 == mMarginEndOfHint) {
            mMarginEndOfHint = dp2px(context, DEFAULT_MARGIN_END_OF_HINT_DP);
        }
        mPaintDivider.setColor(mDividerColor);
        mPaintDivider.setAntiAlias(true);
        mPaintDivider.setStyle(Paint.Style.STROKE);
        mPaintDivider.setStrokeWidth(mDividerHeight);
        mPaintText.setColor(mTextColorNormal);
        mPaintText.setAntiAlias(true);
        mPaintText.setTextAlign(Align.CENTER);
        mPaintHint.setColor(mTextColorHint);
        mPaintHint.setAntiAlias(true);
        mPaintHint.setTextAlign(Align.CENTER);
        mPaintHint.setTextSize(mTextSizeHint);
        if (0 == mShowCount % 2) {
            mShowCount++;
        }
        if (-1 == mMinShowIndex || -1 == mMaxShowIndex) {
            updateValueForInit();
        }
        initHandler();// 初始化 Handler
    }

    private void updateValueForInit() {
        inflateDisplayedValuesIfNull();
        updateWrapStateByContent();
        if (-1 == mMinShowIndex) {
            mMinShowIndex = 0;
        }
        if (-1 == mMaxShowIndex) {
            mMaxShowIndex = mDisplayedValues.length - 1;
        }
        setMinAndMaxShowIndex(mMinShowIndex, mMaxShowIndex, false);
    }

    private void inflateDisplayedValuesIfNull() {
        if (null == mDisplayedValues) {
            mDisplayedValues = new String[1];
            mDisplayedValues[0] = "0";
        }
    }

    private void updateWrapStateByContent() {
        mWrapSelectorWheelCheck = mDisplayedValues.length > mShowCount;
    }

    public void setMinAndMaxShowIndex(int minShowIndex, int maxShowIndex, boolean needRefresh) {
        if (minShowIndex > maxShowIndex) {
            throw new IllegalArgumentException("minShowIndex should be less than maxShowIndex, minShowIndex is " + minShowIndex + ", maxShowIndex is " + maxShowIndex + ".");
        }
        if (null == mDisplayedValues) {
            throw new IllegalArgumentException("mDisplayedValues should not be null, you need to set mDisplayedValues first.");
        } else {
            if (minShowIndex < 0) {
                throw new IllegalArgumentException("minShowIndex should not be less than 0, now minShowIndex is " + minShowIndex);
            } else if (minShowIndex > mDisplayedValues.length - 1) {
                throw new IllegalArgumentException("minShowIndex should not be greater than (mDisplayedValues.length - 1), now " + "(mDisplayedValues.length - 1) is " + (mDisplayedValues.length - 1) + " minShowIndex is " + minShowIndex);
            }
            if (maxShowIndex < 0) {
                throw new IllegalArgumentException("maxShowIndex should not be less than 0, now maxShowIndex is " + maxShowIndex);
            } else if (maxShowIndex > mDisplayedValues.length - 1) {
                throw new IllegalArgumentException("maxShowIndex should not be greater than (mDisplayedValues.length - 1), now " + "(mDisplayedValues.length - 1) is " + (mDisplayedValues.length - 1) + " maxShowIndex is " + maxShowIndex);
            }
        }
        mMinShowIndex = minShowIndex;
        mMaxShowIndex = maxShowIndex;
        if (needRefresh) {
            mPrevPickedIndex = 0 + mMinShowIndex;
            correctPositionByDefaultValue(0, mWrapSelectorWheel && mWrapSelectorWheelCheck);
            postInvalidate();
        }
    }

    //defaultPickedIndex relative to the shown part
    private void correctPositionByDefaultValue(int defaultPickedIndex, boolean wrap) {
        mCurrDrawFirstItemIndex = defaultPickedIndex - (mShowCount - 1) / 2;
        mCurrDrawFirstItemIndex = getIndexByRawIndex(mCurrDrawFirstItemIndex, getOneRecycleSize(), wrap);
        if (0 == mItemHeight) {
            mCurrentItemIndexEffect = true;
        } else {
            mCurrDrawGlobalY = mCurrDrawFirstItemIndex * mItemHeight;
            mInScrollingPickedOldValue = mCurrDrawFirstItemIndex + mShowCount / 2;
            mInScrollingPickedOldValue = mInScrollingPickedOldValue % getOneRecycleSize();
            if (mInScrollingPickedOldValue < 0) {
                mInScrollingPickedOldValue = mInScrollingPickedOldValue + getOneRecycleSize();
            }
            mInScrollingPickedNewValue = mInScrollingPickedOldValue;
            calculateFirstItemParameterByGlobalY();
        }
    }

    private void calculateFirstItemParameterByGlobalY() {
        mCurrDrawFirstItemIndex = (int) Math.floor((float) mCurrDrawGlobalY / mItemHeight);
        mCurrDrawFirstItemY = -(mCurrDrawGlobalY - mCurrDrawFirstItemIndex * mItemHeight);
        if (null != mOnValueChangeListenerInScrolling) {
            mInScrollingPickedNewValue = mCurrDrawFirstItemIndex + mShowCount / 2 + (-mCurrDrawFirstItemY > mItemHeight / 2 ? 1 : 0);
            mInScrollingPickedNewValue = mInScrollingPickedNewValue % getOneRecycleSize();
            if (mInScrollingPickedNewValue < 0) {
                mInScrollingPickedNewValue = mInScrollingPickedNewValue + getOneRecycleSize();
            }
            if (mInScrollingPickedOldValue != mInScrollingPickedNewValue) {
                respondPickedValueChangedInScrolling(mInScrollingPickedOldValue, mInScrollingPickedNewValue);
            }
            mInScrollingPickedOldValue = mInScrollingPickedNewValue;
        }
    }

    private void respondPickedValueChangedInScrolling(int oldValue, int newValue) {
        mOnValueChangeListenerInScrolling.onValueChangeInScrolling(this, oldValue, newValue);
    }

    /**
     * 初始化 Handler
     */
    @SuppressLint("HandlerLeak")
    private void initHandler() {
        mHandlerThread = new HandlerThread("HandlerThread-For-Refreshing");
        mHandlerThread.start();
        mHandlerInNewThread = new Handler(mHandlerThread.getLooper()) {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case HANDLER_WHAT_REFRESH:
                        if (!mScroller.isFinished()) {
                            if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
                                onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
                            }
                            mHandlerInNewThread.sendMessageDelayed(getMsg(HANDLER_WHAT_REFRESH, 0, 0, msg.obj), HANDLER_INTERVAL_REFRESH);
                        } else {
                            int duration = 0;
                            int willPickIndex;
                            //if scroller finished(not scrolling), then adjust the position
                            if (0 != mCurrDrawFirstItemY) {//need to adjust
                                if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
                                    onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
                                }
                                if (mCurrDrawFirstItemY < (-mItemHeight / 2)) {//adjust to scroll upward
                                    duration = (int) ((float) DEFAULT_INTERVAL_REVISE_DURATION * (mItemHeight + mCurrDrawFirstItemY) / mItemHeight);
                                    mScroller.startScroll(0, mCurrDrawGlobalY, 0, mItemHeight + mCurrDrawFirstItemY, duration * 3);
                                    willPickIndex = getWillPickIndexByGlobalY(mCurrDrawGlobalY + mItemHeight + mCurrDrawFirstItemY);
                                } else {//adjust to scroll downward
                                    duration = (int) ((float) DEFAULT_INTERVAL_REVISE_DURATION * (-mCurrDrawFirstItemY) / mItemHeight);
                                    mScroller.startScroll(0, mCurrDrawGlobalY, 0, mCurrDrawFirstItemY, duration * 3);
                                    willPickIndex = getWillPickIndexByGlobalY(mCurrDrawGlobalY + mCurrDrawFirstItemY);
                                }
                                postInvalidate();
                            } else {
                                onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                                willPickIndex = getWillPickIndexByGlobalY(mCurrDrawGlobalY);//get the index which will be selected
                            }
                            Message changeMsg = getMsg(HANDLER_WHAT_LISTENER_VALUE_CHANGED, mPrevPickedIndex, willPickIndex, msg.obj);
                            if (mRespondChangeInMainThread) {
                                mHandlerInMainThread.sendMessageDelayed(changeMsg, duration * 2);
                            } else {
                                mHandlerInNewThread.sendMessageDelayed(changeMsg, duration * 2);
                            }
                        }
                        break;
                    case HANDLER_WHAT_LISTENER_VALUE_CHANGED:
                        respondPickedValueChanged(msg.arg1, msg.arg2, msg.obj);
                        break;
                    default:
                        break;
                }
            }
        };
        mHandlerInMainThread = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                switch (msg.what) {
                    case HANDLER_WHAT_REQUEST_LAYOUT:
                        requestLayout();
                        break;
                    case HANDLER_WHAT_LISTENER_VALUE_CHANGED:
                        respondPickedValueChanged(msg.arg1, msg.arg2, msg.obj);
                        break;
                    default:
                        break;
                }
            }
        };
    }

    //compatible for NumberPicker
    private void onScrollStateChange(int scrollState) {
        if (mScrollState == scrollState) {
            return;
        }
        mScrollState = scrollState;
        if (null != mOnScrollListener) {
            mOnScrollListener.onScrollStateChange(this, scrollState);
        }
    }

    //return index relative to mDisplayedValues from 0.
    private int getWillPickIndexByGlobalY(int globalY) {
        if (0 == mItemHeight) {
            return 0;
        }
        int index = getIndexByRawIndex(globalY / mItemHeight + mShowCount / 2, getOneRecycleSize(), mWrapSelectorWheel && mWrapSelectorWheelCheck);
        if (0 <= index && index < getOneRecycleSize()) {
            return index + mMinShowIndex;
        }/* else {
            throw new IllegalArgumentException("getWillPickIndexByGlobalY illegal index : " + index + " getOneRecycleSize() : " + getOneRecycleSize() + " mWrapSelectorWheel : " + mWrapSelectorWheel);
        }*/
        return index;
    }

    private int getIndexByRawIndex(int index, int size, boolean wrap) {
        if (size <= 0) {
            return 0;
        }
        if (wrap) {
            index = index % size;
            if (index < 0) {
                index = index + size;
            }
        }
        return index;
    }

    /**
     * used by handlers to respond onchange callbacks
     *
     * @param oldVal        prevPicked value
     * @param newVal        currPicked value
     * @param respondChange if want to respond onchange callbacks
     */
    private void respondPickedValueChanged(int oldVal, int newVal, Object respondChange) {
        onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
        if (oldVal != newVal) {
            if (null == respondChange || !(respondChange instanceof Boolean) || (Boolean) respondChange) {
                if (null != mOnValueChangeListener) {
                    mOnValueChangeListener.onValueChange(NumberPickerView.this, oldVal + mMinValue, newVal + mMinValue);
                }
                if (null != mOnValueChangeListenerRaw) {
                    mOnValueChangeListenerRaw.onValueChangeRelativeToRaw(NumberPickerView.this, oldVal, newVal, mDisplayedValues);
                }
            }
        }
        mPrevPickedIndex = newVal;
        if (mPendingWrapToLinear) {
            mPendingWrapToLinear = false;
            internalSetWrapToLinear();
        }
    }

    private void internalSetWrapToLinear() {
        correctPositionByDefaultValue(getPickedIndexRelativeToRaw() - mMinShowIndex, false);
        mWrapSelectorWheel = false;
        postInvalidate();
    }

    /**
     * 2. 测量 View 大小
     * View 的大小不仅由自身所决定，同时也会收到父控件的影响，为了我们的控件能更好的适应各种情况，一般会自己进行测量
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        updateMaxWHOfDisplayedValues(false);
        setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
    }

    private void updateMaxWHOfDisplayedValues(boolean needRequestLayout) {
        updateMaxWidthOfDisplayedValues();
        updateMaxHeightOfDisplayedValues();
        if (needRequestLayout && (mSpecModeW == MeasureSpec.AT_MOST || mSpecModeH == MeasureSpec.AT_MOST)) {
            mHandlerInMainThread.sendEmptyMessage(HANDLER_WHAT_REQUEST_LAYOUT);
        }
    }

    private void updateMaxWidthOfDisplayedValues() {
        float savedTextSize = mPaintText.getTextSize();
        mPaintText.setTextSize(mTextSizeSelected);
        mMaxWidthOfDisplayedValues = getMaxWidthOfTextArray(mDisplayedValues, mPaintText);
        mMaxWidthOfAlterArrayWithMeasureHint = getMaxWidthOfTextArray(mAlterTextArrayWithMeasureHint, mPaintText);
        mMaxWidthOfAlterArrayWithoutMeasureHint = getMaxWidthOfTextArray(mAlterTextArrayWithoutMeasureHint, mPaintText);
        mPaintText.setTextSize(mTextSizeHint);
        mWidthOfAlterHint = getTextWidth(mAlterHint, mPaintText);
        mPaintText.setTextSize(savedTextSize);
    }

    private void updateMaxHeightOfDisplayedValues() {
        float savedTextSize = mPaintText.getTextSize();
        mPaintText.setTextSize(mTextSizeSelected);
        mMaxHeightOfDisplayedValues = (int) (mPaintText.getFontMetrics().bottom - mPaintText.getFontMetrics().top + 0.5);
        mPaintText.setTextSize(savedTextSize);
    }

    /**
     * 3. 确定 View 大小
     * 在视图大小发生改变时调用
     *
     * @param width
     * @param height
     * @param oldWidth
     * @param oldHeight
     */
    @Override
    protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
        super.onSizeChanged(width, height, oldWidth, oldHeight);
        mViewWidth = width;
        mViewHeight = height;
        mItemHeight = mViewHeight / mShowCount;
        mViewCenterX = ((float) (mViewWidth + getPaddingLeft() - getPaddingRight())) / 2;
        int defaultValue = 0;
        if (getOneRecycleSize() > 1) {
            if (mHasInit) {
                defaultValue = getValue() - mMinValue;
            } else if (mCurrentItemIndexEffect) {
                defaultValue = mCurrDrawFirstItemIndex + (mShowCount - 1) / 2;
            } else {
                defaultValue = 0;
            }
        }
        correctPositionByDefaultValue(defaultValue, mWrapSelectorWheel && mWrapSelectorWheelCheck);
        updateFontAttr();
        updateNotWrapYLimit();
        updateDividerAttr();
        mHasInit = true;
    }

    public int getOneRecycleSize() {
        return mMaxShowIndex - mMinShowIndex + 1;
    }

    private void updateFontAttr() {
        if (mTextSizeNormal > mItemHeight) {
            mTextSizeNormal = mItemHeight;
        }
        if (mTextSizeSelected > mItemHeight) {
            mTextSizeSelected = mItemHeight;
        }
        if (null == mPaintHint) {
            throw new IllegalArgumentException("mPaintHint should not be null.");
        }
        mPaintHint.setTextSize(mTextSizeHint);
        mTextSizeHintCenterYOffset = getTextCenterYOffset(mPaintHint.getFontMetrics());
        mWidthOfHintText = getTextWidth(mHintText, mPaintHint);
        if (null == mPaintText) {
            throw new IllegalArgumentException("mPaintText should not be null.");
        }
        mPaintText.setTextSize(mTextSizeSelected);
        mTextSizeSelectedCenterYOffset = getTextCenterYOffset(mPaintText.getFontMetrics());
        mPaintText.setTextSize(mTextSizeNormal);
        mTextSizeNormalCenterYOffset = getTextCenterYOffset(mPaintText.getFontMetrics());
    }

    private float getTextCenterYOffset(Paint.FontMetrics fontMetrics) {
        return null == fontMetrics ? 0 : Math.abs(fontMetrics.top + fontMetrics.bottom) / 2;
    }

    private int getTextWidth(CharSequence text, Paint paint) {
        return TextUtils.isEmpty(text) ? 0 : (int) (paint.measureText(text.toString()) + 0.5f);
    }

    private void updateNotWrapYLimit() {
        mNotWrapLimitYTop = 0;
        mNotWrapLimitYBottom = -mShowCount * mItemHeight;
        if (null != mDisplayedValues) {
            mNotWrapLimitYTop = (getOneRecycleSize() - mShowCount / 2 - 1) * mItemHeight;
            mNotWrapLimitYBottom = -(mShowCount / 2) * mItemHeight;
        }
    }

    private void updateDividerAttr() {
        int mDividerIndex0 = mShowCount / 2;
        int mDividerIndex1 = mDividerIndex0 + 1;
        dividerY0 = mDividerIndex0 * mViewHeight / mShowCount;
        dividerY1 = mDividerIndex1 * mViewHeight / mShowCount;
        if (mDividerMarginL < 0) {
            mDividerMarginL = 0;
        }
        if (mDividerMarginR < 0) mDividerMarginR = 0;
        if (mDividerMarginL + mDividerMarginR == 0) {
            return;
        }
        if (getPaddingLeft() + mDividerMarginL >= mViewWidth - getPaddingRight() - mDividerMarginR) {
            int surplusMargin = getPaddingLeft() + mDividerMarginL + getPaddingRight() + mDividerMarginR - mViewWidth;
            mDividerMarginL = (int) (mDividerMarginL - (float) surplusMargin * mDividerMarginL / (mDividerMarginL + mDividerMarginR));
            mDividerMarginR = (int) (mDividerMarginR - (float) surplusMargin * mDividerMarginR / (mDividerMarginL + mDividerMarginR));
        }
    }

    /**
     * 4.确定子 View（包含子 View 时用）
     *
     * @param changed
     * @param left    View 左侧距父 View 左侧的距离
     * @param top     View 顶部距父 View 顶部的距离
     * @param right   View 右侧距父 View 左侧的距离
     * @param bottom  View 底部距父 View 顶部的距离
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

    /**
     * 5.绘制实际内容
     * 实际绘制的部分，使用 Canvas 绘画
     * 绘制的基本形状由 Canvas 确定，绘制出来的颜色、具体效果由 Paint 确定
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawContent(canvas);
        drawLine(canvas);
        drawHint(canvas);
    }

    private void drawContent(Canvas canvas) {
        int index;
        int textColor;
        float textSize;
        float fraction = 0f;// fraction of the item in state between normal and selected, in[0, 1]
        float textSizeCenterYOffset;
        for (int i = 0; i < mShowCount + 1; i++) {
            float y = mCurrDrawFirstItemY + mItemHeight * i;
            index = getIndexByRawIndex(mCurrDrawFirstItemIndex + i, getOneRecycleSize(), mWrapSelectorWheel && mWrapSelectorWheelCheck);
            if (i == mShowCount / 2) {//this will be picked
                fraction = (float) (mItemHeight + mCurrDrawFirstItemY) / mItemHeight;
                textColor = getEvaluateColor(fraction, mTextColorNormal, mTextColorSelected);
                textSize = getEvaluateSize(fraction, mTextSizeNormal, mTextSizeSelected);
                textSizeCenterYOffset = getEvaluateSize(fraction, mTextSizeNormalCenterYOffset, mTextSizeSelectedCenterYOffset);
            } else if (i == mShowCount / 2 + 1) {
                textColor = getEvaluateColor(1 - fraction, mTextColorNormal, mTextColorSelected);
                textSize = getEvaluateSize(1 - fraction, mTextSizeNormal, mTextSizeSelected);
                textSizeCenterYOffset = getEvaluateSize(1 - fraction, mTextSizeNormalCenterYOffset, mTextSizeSelectedCenterYOffset);
            } else {
                textColor = mTextColorNormal;
                textSize = mTextSizeNormal;
                textSizeCenterYOffset = mTextSizeNormalCenterYOffset;
            }
            mPaintText.setColor(textColor);
            mPaintText.setTextSize(textSize);
            if (index >= 0 && index < getOneRecycleSize()) {
                CharSequence str = mDisplayedValues[index + mMinShowIndex];
                if (null != mTextEllipsize) {
                    str = TextUtils.ellipsize(str, mPaintText, getWidth() - 2 * mItemPaddingHorizontal, getEllipsizeType());
                }
                canvas.drawText(str.toString(), mViewCenterX, y + mItemHeight / 2 + textSizeCenterYOffset, mPaintText);
            } else if (!TextUtils.isEmpty(mEmptyItemHint)) {
                canvas.drawText(mEmptyItemHint, mViewCenterX, y + mItemHeight / 2 + textSizeCenterYOffset, mPaintText);
            }
        }
    }

    private void drawLine(Canvas canvas) {
        if (mShowDivider) {
            canvas.drawLine(getPaddingLeft() + mDividerMarginL, dividerY0, mViewWidth - getPaddingRight() - mDividerMarginR, dividerY0, mPaintDivider);
            canvas.drawLine(getPaddingLeft() + mDividerMarginL, dividerY1, mViewWidth - getPaddingRight() - mDividerMarginR, dividerY1, mPaintDivider);
        }
    }

    private void drawHint(Canvas canvas) {
        if (TextUtils.isEmpty(mHintText)) {
            return;
        }
        canvas.drawText(mHintText, mViewCenterX + (mMaxWidthOfDisplayedValues + mWidthOfHintText) / 2 + mMarginStartOfHint, (dividerY0 + dividerY1) / 2 + mTextSizeHintCenterYOffset, mPaintHint);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (null == mHandlerThread || !mHandlerThread.isAlive()) {
            initHandler();
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mHandlerThread.quit();
        //These codes are for dialog or PopupWindow which will be used for more than once. Not an elegant solution, if you have any good idea, please let me know, thank you.
        if (mItemHeight == 0) return;
        if (!mScroller.isFinished()) {
            mScroller.abortAnimation();
            mCurrDrawGlobalY = mScroller.getCurrY();
            calculateFirstItemParameterByGlobalY();
            if (0 != mCurrDrawFirstItemY) {
                mCurrDrawGlobalY = mCurrDrawGlobalY + mCurrDrawFirstItemY + mCurrDrawFirstItemY < (-mItemHeight / 2) ? mItemHeight : 0;
                calculateFirstItemParameterByGlobalY();
            }
            onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
        }
        // see the comments on mRespondChangeOnDetach, if mRespondChangeOnDetach is false, please initialize NumberPickerView's data every time setting up NumberPickerView, set the demo of GregorianLunarCalendar
        int currPickedIndex = getWillPickIndexByGlobalY(mCurrDrawGlobalY);
        if (currPickedIndex != mPrevPickedIndex && mRespondChangeOnDetach) {
            try {
                if (null != mOnValueChangeListener) {
                    mOnValueChangeListener.onValueChange(NumberPickerView.this, mPrevPickedIndex + mMinValue, currPickedIndex + mMinValue);
                }
                if (null != mOnValueChangeListenerRaw) {
                    mOnValueChangeListenerRaw.onValueChangeRelativeToRaw(NumberPickerView.this, mPrevPickedIndex, currPickedIndex, mDisplayedValues);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        mPrevPickedIndex = currPickedIndex;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (0 == mItemHeight) {
            return true;
        }
        if (null == mVelocityTracker) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);
        currY = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mFlagMayPress = true;
                mHandlerInNewThread.removeMessages(HANDLER_WHAT_REFRESH);
                stopScrolling();
                downY = currY;
                downYGlobal = mCurrDrawGlobalY;
                onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                float spanY = downY - currY;
                if (mFlagMayPress && (-mScaledTouchSlop < spanY && spanY < mScaledTouchSlop)) {
                } else {
                    mFlagMayPress = false;
                    mCurrDrawGlobalY = limitY((int) (downYGlobal + spanY));
                    calculateFirstItemParameterByGlobalY();
                    invalidate();
                }
                onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
                break;
            case MotionEvent.ACTION_UP:
                if (mFlagMayPress) {
                    click(event);
                } else {
                    final VelocityTracker velocityTracker = mVelocityTracker;
                    velocityTracker.computeCurrentVelocity(1000);
                    int velocityY = (int) (velocityTracker.getYVelocity() * mFriction);
                    if (Math.abs(velocityY) > mMiniVelocityFling) {
                        mScroller.fling(0, mCurrDrawGlobalY, 0, -velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, limitY(Integer.MIN_VALUE), limitY(Integer.MAX_VALUE));
                        invalidate();
                        onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
                    }
                    mHandlerInNewThread.sendMessageDelayed(getMsg(HANDLER_WHAT_REFRESH), 0);
                    releaseVelocityTracker();
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                downYGlobal = mCurrDrawGlobalY;
                stopScrolling();
                mHandlerInNewThread.sendMessageDelayed(getMsg(HANDLER_WHAT_REFRESH), 0);
                break;
        }
        return true;
    }

    private void releaseVelocityTracker() {
        if (null != mVelocityTracker) {
            mVelocityTracker.clear();
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

    public int getRawContentSize() {
        return null == mDisplayedValues ? 0 : mDisplayedValues.length;
    }

    public void setDisplayedValuesAndPickedIndex(String[] newDisplayedValues, int pickedIndex, boolean needRefresh) {
        stopScrolling();
        if (null == newDisplayedValues) {
            throw new IllegalArgumentException("newDisplayedValues should not be null.");
        }
        if (pickedIndex < 0) {
            throw new IllegalArgumentException("pickedIndex should not be negative, now pickedIndex is " + pickedIndex);
        }
        updateContent(newDisplayedValues);
        updateMaxWHOfDisplayedValues(true);
        updateNotWrapYLimit();
        updateValue();
        mPrevPickedIndex = pickedIndex + mMinShowIndex;
        correctPositionByDefaultValue(pickedIndex, mWrapSelectorWheel && mWrapSelectorWheelCheck);
        if (needRefresh) {
            mHandlerInNewThread.sendMessageDelayed(getMsg(HANDLER_WHAT_REFRESH), 0);
            postInvalidate();
        }
    }

    public void setDisplayedValues(String[] newDisplayedValues, boolean needRefresh) {
        setDisplayedValuesAndPickedIndex(newDisplayedValues, 0, needRefresh);
    }

    public void setDisplayedValues(String[] newDisplayedValues) {
        stopRefreshing();
        stopScrolling();
        if (null == newDisplayedValues) {
            throw new IllegalArgumentException("newDisplayedValues should not be null.");
        }
        if (mMaxValue - mMinValue + 1 > newDisplayedValues.length) {
            throw new IllegalArgumentException("mMaxValue - mMinValue + 1 should not be greater than mDisplayedValues.length, now " + "((mMaxValue - mMinValue + 1) is "
                    + (mMaxValue - mMinValue + 1) + " newDisplayedValues.length is " + newDisplayedValues.length + ", you need to set MaxValue and MinValue before setDisplayedValues(String[])");
        }
        updateContent(newDisplayedValues);
        updateMaxWHOfDisplayedValues(true);
        mPrevPickedIndex = 0 + mMinShowIndex;
        correctPositionByDefaultValue(0, mWrapSelectorWheel && mWrapSelectorWheelCheck);
        postInvalidate();
        mHandlerInMainThread.sendEmptyMessage(HANDLER_WHAT_REQUEST_LAYOUT);
    }

    /**
     * Gets the values to be displayed instead of string values.
     *
     * @return The displayed values.
     */
    public String[] getDisplayedValues() {
        return mDisplayedValues;
    }

    public void setWrapSelectorWheel(boolean wrapSelectorWheel) {
        if (mWrapSelectorWheel != wrapSelectorWheel) {
            if (!wrapSelectorWheel) {
                if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
                    internalSetWrapToLinear();
                } else {
                    mPendingWrapToLinear = true;
                }
            } else {
                mWrapSelectorWheel = wrapSelectorWheel;
                updateWrapStateByContent();
                postInvalidate();
            }
        }
    }

    /**
     * get the "fromValue" by using getValue(), if your picker's minValue is not 0, make sure you can get the accurate
     * value by getValue(), or you can use smoothScrollToValue(int fromValue, int toValue, boolean needRespond)
     *
     * @param toValue the value you want picker to scroll to
     */
    public void smoothScrollToValue(int toValue) {
        smoothScrollToValue(getValue(), toValue, true);
    }

    /**
     * get the "fromValue" by using getValue(), if your picker's minValue is not 0,
     * make sure you can get the accurate value by getValue(), or you can use
     * smoothScrollToValue(int fromValue, int toValue, boolean needRespond)
     *
     * @param toValue     the value you want picker to scroll to
     * @param needRespond set if you want picker to respond onValueChange listener
     */
    public void smoothScrollToValue(int toValue, boolean needRespond) {
        smoothScrollToValue(getValue(), toValue, needRespond);
    }

    public void smoothScrollToValue(int fromValue, int toValue) {
        smoothScrollToValue(fromValue, toValue, true);
    }

    /**
     * @param fromValue   need to set the fromValue, can be greater than mMaxValue or less than mMinValue
     * @param toValue     the value you want picker to scroll to
     * @param needRespond need Respond to the ValueChange callback When Scrolling, default is false
     */
    public void smoothScrollToValue(int fromValue, int toValue, boolean needRespond) {
        int deltaIndex;
        fromValue = refineValueByLimit(fromValue, mMinValue, mMaxValue, mWrapSelectorWheel && mWrapSelectorWheelCheck);
        toValue = refineValueByLimit(toValue, mMinValue, mMaxValue, mWrapSelectorWheel && mWrapSelectorWheelCheck);
        if (mWrapSelectorWheel && mWrapSelectorWheelCheck) {
            deltaIndex = toValue - fromValue;
            int halfOneRecycleSize = getOneRecycleSize() / 2;
            if (deltaIndex < -halfOneRecycleSize || halfOneRecycleSize < deltaIndex) {
                deltaIndex = deltaIndex > 0 ? deltaIndex - getOneRecycleSize() : deltaIndex + getOneRecycleSize();
            }
        } else {
            deltaIndex = toValue - fromValue;
        }
        setValue(fromValue);
        if (fromValue == toValue) {
            return;
        }
        scrollByIndexSmoothly(deltaIndex, needRespond);
    }

    /**
     * simplify the "setDisplayedValue() + setMinValue() + setMaxValue()" process,
     * default minValue is 0, and make sure you do NOT change the minValue.
     *
     * @param display new values to be displayed
     */
    public void refreshByNewDisplayedValues(String[] display) {
        int minValue = getMinValue();
        int oldMaxValue = getMaxValue();
        int oldSpan = oldMaxValue - minValue + 1;
        int newMaxValue = display.length - 1;
        int newSpan = newMaxValue - minValue + 1;
        if (newSpan > oldSpan) {
            setDisplayedValues(display);
            setMaxValue(newMaxValue);
        } else {
            setMaxValue(newMaxValue);
            setDisplayedValues(display);
        }
    }

    private void scrollByIndexSmoothly(int deltaIndex) {
        scrollByIndexSmoothly(deltaIndex, true);
    }

    /**
     * @param deltaIndex  the delta index it will scroll by
     * @param needRespond need Respond to the ValueChange callback When Scrolling, default is false
     */
    private void scrollByIndexSmoothly(int deltaIndex, boolean needRespond) {
        if (!(mWrapSelectorWheel && mWrapSelectorWheelCheck)) {
            int willPickRawIndex = getPickedIndexRelativeToRaw();
            if (willPickRawIndex + deltaIndex > mMaxShowIndex) {
                deltaIndex = mMaxShowIndex - willPickRawIndex;
            } else if (willPickRawIndex + deltaIndex < mMinShowIndex) {
                deltaIndex = mMinShowIndex - willPickRawIndex;
            }
        }
        int duration;
        int dy;
        if (mCurrDrawFirstItemY < (-mItemHeight / 2)) {//scroll upwards for a distance of less than mItemHeight
            dy = mItemHeight + mCurrDrawFirstItemY;
            duration = (int) ((float) DEFAULT_INTERVAL_REVISE_DURATION * (mItemHeight + mCurrDrawFirstItemY) / mItemHeight);
            if (deltaIndex < 0) {
                duration = -duration - deltaIndex * DEFAULT_INTERVAL_REVISE_DURATION;
            } else {
                duration = duration + deltaIndex * DEFAULT_INTERVAL_REVISE_DURATION;
            }
        } else {//scroll downwards for a distance of less than mItemHeight
            dy = mCurrDrawFirstItemY;
            duration = (int) ((float) DEFAULT_INTERVAL_REVISE_DURATION * (-mCurrDrawFirstItemY) / mItemHeight);
            if (deltaIndex < 0) {
                duration = duration - deltaIndex * DEFAULT_INTERVAL_REVISE_DURATION;
            } else {
                duration = duration + deltaIndex * DEFAULT_INTERVAL_REVISE_DURATION;
            }
        }
        dy = dy + deltaIndex * mItemHeight;
        if (duration < DEFAULT_MIN_SCROLL_BY_INDEX_DURATION) {
            duration = DEFAULT_MIN_SCROLL_BY_INDEX_DURATION;
        }
        if (duration > DEFAULT_MAX_SCROLL_BY_INDEX_DURATION) {
            duration = DEFAULT_MAX_SCROLL_BY_INDEX_DURATION;
        }
        mScroller.startScroll(0, mCurrDrawGlobalY, 0, dy, duration);
        if (needRespond) {
            mHandlerInNewThread.sendMessageDelayed(getMsg(HANDLER_WHAT_REFRESH), duration / 4);
        } else {
            mHandlerInNewThread.sendMessageDelayed(getMsg(HANDLER_WHAT_REFRESH, 0, 0, new Boolean(needRespond)), duration / 4);
        }
        postInvalidate();
    }

    public int getMinValue() {
        return mMinValue;
    }

    public int getMaxValue() {
        return mMaxValue;
    }

    public void setMinValue(int minValue) {
        mMinValue = minValue;
        mMinShowIndex = 0;
        updateNotWrapYLimit();
    }

    //compatible for android.widget.NumberPicker
    public void setMaxValue(int maxValue) {
        if (null == mDisplayedValues) {
            throw new NullPointerException("mDisplayedValues should not be null");
        }
        if (maxValue - mMinValue + 1 > mDisplayedValues.length) {
            throw new IllegalArgumentException("(maxValue - mMinValue + 1) should not be greater than mDisplayedValues.length now " +
                    " (maxValue - mMinValue + 1) is " + (maxValue - mMinValue + 1) + " and mDisplayedValues.length is " + mDisplayedValues.length);
        }
        mMaxValue = maxValue;
        mMaxShowIndex = mMaxValue - mMinValue + mMinShowIndex;
        setMinAndMaxShowIndex(mMinShowIndex, mMaxShowIndex);
        updateNotWrapYLimit();
    }

    //compatible for android.widget.NumberPicker
    public void setValue(int value) {
        if (value < mMinValue) {
            throw new IllegalArgumentException("should not set a value less than mMinValue, value is " + value);
        }
        if (value > mMaxValue) {
            throw new IllegalArgumentException("should not set a value greater than mMaxValue, value is " + value);
        }
        setPickedIndexRelativeToRaw(value - mMinValue);
    }

    //compatible for android.widget.NumberPicker
    public int getValue() {
        return getPickedIndexRelativeToRaw() + mMinValue;
    }

    public String getContentByCurrValue() {
        return mDisplayedValues[getValue() - mMinValue];
    }

    public boolean getWrapSelectorWheel() {
        return mWrapSelectorWheel;
    }

    public boolean getWrapSelectorWheelAbsolutely() {
        return mWrapSelectorWheel && mWrapSelectorWheelCheck;
    }

    public void setHintText(String hintText) {
        if (isStringEqual(mHintText, hintText)) {
            return;
        }
        mHintText = hintText;
        mTextSizeHintCenterYOffset = getTextCenterYOffset(mPaintHint.getFontMetrics());
        mWidthOfHintText = getTextWidth(mHintText, mPaintHint);
        mHandlerInMainThread.sendEmptyMessage(HANDLER_WHAT_REQUEST_LAYOUT);
    }

    public void setPickedIndexRelativeToMin(int pickedIndexToMin) {
        if (0 <= pickedIndexToMin && pickedIndexToMin < getOneRecycleSize()) {
            mPrevPickedIndex = pickedIndexToMin + mMinShowIndex;
            correctPositionByDefaultValue(pickedIndexToMin, mWrapSelectorWheel && mWrapSelectorWheelCheck);
            postInvalidate();
        }
    }

    public void setNormalTextColor(int normalTextColor) {
        if (mTextColorNormal == normalTextColor) {
            return;
        }
        mTextColorNormal = normalTextColor;
        postInvalidate();
    }

    public void setSelectedTextColor(int selectedTextColor) {
        if (mTextColorSelected == selectedTextColor) {
            return;
        }
        mTextColorSelected = selectedTextColor;
        postInvalidate();
    }

    public void setHintTextColor(int hintTextColor) {
        if (mTextColorHint == hintTextColor) {
            return;
        }
        mTextColorHint = hintTextColor;
        mPaintHint.setColor(mTextColorHint);
        postInvalidate();
    }

    public void setDividerColor(int dividerColor) {
        if (mDividerColor == dividerColor) {
            return;
        }
        mDividerColor = dividerColor;
        mPaintDivider.setColor(mDividerColor);
        postInvalidate();
    }

    public void setPickedIndexRelativeToRaw(int pickedIndexToRaw) {
        if (mMinShowIndex > -1) {
            if (mMinShowIndex <= pickedIndexToRaw && pickedIndexToRaw <= mMaxShowIndex) {
                mPrevPickedIndex = pickedIndexToRaw;
                correctPositionByDefaultValue(pickedIndexToRaw - mMinShowIndex, mWrapSelectorWheel && mWrapSelectorWheelCheck);
                postInvalidate();
            }
        }
    }

    public int getPickedIndexRelativeToRaw() {
        int willPickIndex;
        if (0 != mCurrDrawFirstItemY) {
            willPickIndex = getWillPickIndexByGlobalY(mCurrDrawGlobalY + mCurrDrawFirstItemY + mCurrDrawFirstItemY < (-mItemHeight / 2) ? mItemHeight : 0);
        } else {
            willPickIndex = getWillPickIndexByGlobalY(mCurrDrawGlobalY);
        }
        return willPickIndex;
    }

    public void setMinAndMaxShowIndex(int minShowIndex, int maxShowIndex) {
        setMinAndMaxShowIndex(minShowIndex, maxShowIndex, true);
    }

    /**
     * set the friction of scroller, it will effect the scroller's acceleration when fling
     *
     * @param friction default is ViewConfiguration.get(mContext).getScrollFriction()
     *                 if setFriction(2 * ViewConfiguration.get(mContext).getScrollFriction()),
     *                 the friction will be twice as much as before
     */
    public void setFriction(float friction) {
        if (friction <= 0) {
            throw new IllegalArgumentException("you should set a a positive float friction, now friction is " + friction);
        }
        mFriction = ViewConfiguration.get(getContext()).getScrollFriction() / friction;
    }

    public void setContentTextTypeface(Typeface typeface) {
        mPaintText.setTypeface(typeface);
    }

    public void setHintTextTypeface(Typeface typeface) {
        mPaintHint.setTypeface(typeface);
    }

    private int limitY(int currDrawGlobalYPreferred) {
        if (mWrapSelectorWheel && mWrapSelectorWheelCheck) {
            return currDrawGlobalYPreferred;
        }
        if (currDrawGlobalYPreferred < mNotWrapLimitYBottom) {
            currDrawGlobalYPreferred = mNotWrapLimitYBottom;
        } else if (currDrawGlobalYPreferred > mNotWrapLimitYTop) {
            currDrawGlobalYPreferred = mNotWrapLimitYTop;
        }
        return currDrawGlobalYPreferred;
    }

    private void click(MotionEvent event) {
        float y = event.getY();
        for (int i = 0; i < mShowCount; i++) {
            if (mItemHeight * i <= y && y < mItemHeight * (i + 1)) {
                clickItem(i);
                break;
            }
        }
    }

    private void clickItem(int showCountIndex) {
        if (0 <= showCountIndex && showCountIndex < mShowCount) {
            scrollByIndexSmoothly(showCountIndex - mShowCount / 2);//clicked the showCountIndex of the view
        } else {
            //wrong
        }
    }

    @Override
    public void computeScroll() {
        if (0 == mItemHeight) {
            return;
        }
        if (mScroller.computeScrollOffset()) {
            mCurrDrawGlobalY = mScroller.getCurrY();
            calculateFirstItemParameterByGlobalY();
            postInvalidate();
        }
    }

    private int measureWidth(int measureSpec) {
        int result;
        int specMode = mSpecModeW = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            int marginOfHint = 0 == Math.max(mWidthOfHintText, mWidthOfAlterHint) ? 0 : mMarginEndOfHint;
            int gapOfHint = 0 == Math.max(mWidthOfHintText, mWidthOfAlterHint) ? 0 : mMarginStartOfHint;
            int maxWidth = Math.max(mMaxWidthOfAlterArrayWithMeasureHint, Math.max(mMaxWidthOfDisplayedValues, mMaxWidthOfAlterArrayWithoutMeasureHint)
                    + 2 * (gapOfHint + Math.max(mWidthOfHintText, mWidthOfAlterHint) + marginOfHint + 2 * mItemPaddingHorizontal));
            result = this.getPaddingLeft() + this.getPaddingRight() + maxWidth;//MeasureSpec.UNSPECIFIED
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    private int measureHeight(int measureSpec) {
        int result;
        int specMode = mSpecModeH = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            result = this.getPaddingTop() + this.getPaddingBottom() + mShowCount * (mMaxHeightOfDisplayedValues + 2 * mItemPaddingVertical);//MeasureSpec.UNSPECIFIED
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    private TextUtils.TruncateAt getEllipsizeType() {
        switch (mTextEllipsize) {
            case TEXT_ELLIPSIZE_START:
                return TextUtils.TruncateAt.START;
            case TEXT_ELLIPSIZE_MIDDLE:
                return TextUtils.TruncateAt.MIDDLE;
            case TEXT_ELLIPSIZE_END:
                return TextUtils.TruncateAt.END;
            default:
                throw new IllegalArgumentException("Illegal text ellipsize type.");
        }
    }

    private int getMaxWidthOfTextArray(CharSequence[] array, Paint paint) {
        if (null == array) {
            return 0;
        }
        int maxWidth = 0;
        for (CharSequence item : array) {
            if (null != item) {
                maxWidth = Math.max(getTextWidth(item, paint), maxWidth);
            }
        }
        return maxWidth;
    }

    private void updateContentAndIndex(String[] newDisplayedValues) {
        mMinShowIndex = 0;
        mMaxShowIndex = newDisplayedValues.length - 1;
        mDisplayedValues = newDisplayedValues;
        updateWrapStateByContent();
    }

    private void updateContent(String[] newDisplayedValues) {
        mDisplayedValues = newDisplayedValues;
        updateWrapStateByContent();
    }

    //used in setDisplayedValues
    private void updateValue() {
        inflateDisplayedValuesIfNull();
        updateWrapStateByContent();
        mMinShowIndex = 0;
        mMaxShowIndex = mDisplayedValues.length - 1;
    }

    private int refineValueByLimit(int value, int minValue, int maxValue, boolean wrap) {
        if (wrap) {
            if (value > maxValue) {
                value = (value - maxValue) % getOneRecycleSize() + minValue - 1;
            } else if (value < minValue) {
                value = (value - minValue) % getOneRecycleSize() + maxValue + 1;
            }
            return value;
        } else {
            if (value > maxValue) {
                value = maxValue;
            } else if (value < minValue) {
                value = minValue;
            }
            return value;
        }
    }

    private void stopRefreshing() {
        if (null != mHandlerInNewThread) {
            mHandlerInNewThread.removeMessages(HANDLER_WHAT_REFRESH);
        }
    }

    public void stopScrolling() {
        if (null != mScroller) {
            if (!mScroller.isFinished()) {
                mScroller.startScroll(0, mScroller.getCurrY(), 0, 0, 1);
                mScroller.abortAnimation();
                postInvalidate();
            }
        }
    }

    public void stopScrollingAndCorrectPosition() {
        stopScrolling();
        if (null != mHandlerInNewThread) {
            mHandlerInNewThread.sendMessageDelayed(getMsg(HANDLER_WHAT_REFRESH), 0);
        }
    }

    private Message getMsg(int what) {
        return getMsg(what, 0, 0, null);
    }

    private Message getMsg(int what, int arg1, int arg2, Object obj) {
        Message msg = Message.obtain();
        msg.what = what;
        msg.arg1 = arg1;
        msg.arg2 = arg2;
        msg.obj = obj;
        return msg;
    }

    //===tool functions===//
    private boolean isStringEqual(String a, String b) {
        return null == a ? null == b : a.equals(b);
    }

    private int getEvaluateColor(float fraction, int startColor, int endColor) {
        int sA = (startColor & 0xff000000) >>> 24;
        int sR = (startColor & 0x00ff0000) >>> 16;
        int sG = (startColor & 0x0000ff00) >>> 8;
        int sB = (startColor & 0x000000ff) >>> 0;
        int eA = (endColor & 0xff000000) >>> 24;
        int eR = (endColor & 0x00ff0000) >>> 16;
        int eG = (endColor & 0x0000ff00) >>> 8;
        int eB = (endColor & 0x000000ff) >>> 0;
        return ((int) (sA + (eA - sA) * fraction) << 24) | ((int) (sR + (eR - sR) * fraction) << 16) | ((int) (sG + (eG - sG) * fraction) << 8) | ((int) (sB + (eB - sB) * fraction));
    }

    private float getEvaluateSize(float fraction, float startSize, float endSize) {
        return startSize + (endSize - startSize) * fraction;
    }

    private String[] convertCharSequenceArrayToStringArray(CharSequence[] charSequences) {
        if (null == charSequences) {
            return null;
        }
        String[] ret = new String[charSequences.length];
        for (int i = 0; i < charSequences.length; i++) {
            ret[i] = charSequences[i].toString();
        }
        return ret;
    }

    //compatible for NumberPicker
    public void setOnValueChangedListener(OnValueChangeListener listener) {
        mOnValueChangeListener = listener;
    }

    public void setOnValueChangedListenerRelativeToRaw(OnValueChangeListenerRelativeToRaw listener) {
        mOnValueChangeListenerRaw = listener;
    }

    public void setOnValueChangeListenerInScrolling(OnValueChangeListenerInScrolling listener) {
        mOnValueChangeListenerInScrolling = listener;
    }

    //compatible for NumberPicker
    public void setOnScrollListener(OnScrollListener listener) {
        mOnScrollListener = listener;
    }

    // compatible for NumberPicker
    public interface OnValueChangeListener {
        void onValueChange(NumberPickerView picker, int oldValue, int newValue);
    }

    public interface OnValueChangeListenerRelativeToRaw {
        void onValueChangeRelativeToRaw(NumberPickerView picker, int oldPickedIndex, int newPickedIndex, String[] displayedValues);
    }

    public interface OnValueChangeListenerInScrolling {
        void onValueChangeInScrolling(NumberPickerView picker, int oldValue, int newValue);
    }

    // compatible for NumberPicker
    public interface OnScrollListener {
        int SCROLL_STATE_IDLE = 0;
        int SCROLL_STATE_TOUCH_SCROLL = 1;
        int SCROLL_STATE_FLING = 2;

        void onScrollStateChange(NumberPickerView view, int scrollState);
    }

    private int sp2px(Context context, float spValue) {
        return (int) (spValue * (context.getResources().getDisplayMetrics().scaledDensity) + 0.5f);
    }

    private int dp2px(Context context, float dpValue) {
        return (int) (dpValue * (context.getResources().getDisplayMetrics().density) + 0.5f);
    }

}